import os
import io  # 获取io流内容
import pandas as pd

from utils.log import Logger
from utils.common import data_processing
import datetime
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder,StandardScaler
from sklearn.feature_selection import VarianceThreshold
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'SimHei'
plt.rcParams['font.size'] = 15
class root_data(object):
    # 主类获取数据并创建日志对象
    def __init__(self,data_path):
        # 使用名称+当前时间是为了避免名字重复而覆盖文件
        log_name = 'feature_pro' + datetime.datetime.now().strftime('%Y%m%d%H%M%S')
        # 初始化日志文件对象
        self.log_file = Logger('../log', log_name).get_logger()
        # 获取源数据(应当统一在common写逻辑)
        self.data_source = data_processing(data_path)

def feature_onehot_standar(data,logger,drop_column = None,categorical_data=None,label_column='Attrition',model_type='tree'):
    '''
    通用OneHot编码处理器
        示例用法：

        示例1：使用默认参数（树模型，自动删除默认列，自动编码默认分类列）
            X, y = feature_onehot_standar(data, logger)

        示例2：自定义删除列 + 编码列 + 标签列（适合字段名不同的数据集）
            X, y = feature_onehot_standar(
                data, logger,
                drop_column=['EmployeeID', 'Gender'],
                categorical_data=['部门', '职位', '婚姻状态'],
                label_column='是否离职',
                model_type='linear'
            )

        示例3：仅修改模型类型为线性模型（会自动对数值特征标准化）
            X, y = feature_onehot_standar(data, logger, model_type='linear')

        示例4：不删除任何列，仅编码指定列
            X, y = feature_onehot_standar(
                data, logger,
                drop_column=[],
                categorical_data=['JobRole', 'MaritalStatus']
            )
    :param data: 传入源数据
    :param logger: 传入日志对象，用来记录处理过程
    :param drop_column: 传入想要进行删除的特征，不传入使用默认值
    :param categorical_data: 传入想要进行onehot编码的特征，不传入使用默认值
    :param label_column: 传入标签，可自定义，不自定义则使用默认值
    :param model_type: 模型类型，'tree'（如XGBoost, RF）或 'linear'（如Logistic, SVM），默认'tree'
    :return:x(DataFrame): x_encod_df 返回经过onehot编码处理后的特征数据 ，y(Series):返回为标签

    '''
    # 进行特征处理并分离数据
    logger.info(f'------------正在进行数据处理-------------')
    data2 = data.copy()
    # 定义默认删除的特征
    if drop_column == None:
        drop_column = ['EmployeeNumber', 'Over18', 'StandardHours']
    # 动态删除所需要删除的列并使用安全检查查看是否在当前数据中有这个特征
    existing_drop_col = [col for col in drop_column if col in data2.columns]
    if existing_drop_col:
        data2.drop(existing_drop_col,axis=1,inplace=True)
        logger.info(f'删除这些列:{existing_drop_col}')
    else:
        logger.warning(f'警告，当前未删除任何列，传入的特征名不存在于数据中')
    # 捕获控制台输出内容并转换为字符串输出
    buffer = io.StringIO()
    data2.info(buf=buffer)
    info_str = buffer.getvalue()
    logger.info(f'数据基本信息为:\n{info_str}')
    logger.info(f'正在获取源数据,前五条为:\n{data2.head().to_string()}')  # 使用to_string可以避免被省略内容
    if categorical_data ==None:
        # 设置默认分类特征
        categorical_data = [  # 分离出分类型数据,单独对这些特征使用热编码
            'BusinessTravel',
            'Department',
            'EducationField',
            'Gender',
            'JobRole',
            'MaritalStatus',
            'OverTime'
            ]
    existing_onehot_cols = [col for col in categorical_data if col in data2.columns]
    if not existing_onehot_cols:
        logger.warning(f'未找到任何特征可进行onehot编码')
    else:
        logger.info(f'将对以下特征进行onehot编码{existing_onehot_cols}')

    if label_column not in data2.columns:
        raise ValueError(f"数据中没有标签列 '{label_column}'")
    # 获取除了标签外的所有列
    x = data2.drop(label_column,axis=1)
    y = data2[label_column]  # 这里并没有用到这个，只是用来区别特征和标签,可去掉
    # 找出要标准化的特征
    numeric_feature = x.select_dtypes(include=['int64','float64']).columns.tolist()
    # 排除掉经过One_Hot编码的
    numeric_feature = [col for col in numeric_feature if col not in existing_onehot_cols]
    logger.info(f'将对以下特征进行标准化:{numeric_feature}')
    transformers = []
    if existing_onehot_cols:
        transformers.append(
            ('one_hot', OneHotEncoder(drop='first', sparse_output=False), existing_onehot_cols)
        )
    if model_type != 'tree' and numeric_feature:
        transformers.append((
            'scaler', StandardScaler(), numeric_feature
        ))
        logger.info(f'[线性模型] 将对以下特征进行标准化: {numeric_feature}')
    elif model_type == 'tree' and numeric_feature:
        logger.info(f'[树模型] 跳过标准化，保留原始数值特征: {numeric_feature}')
    preprocessor = ColumnTransformer(
        transformers=transformers,
        remainder='passthrough'
    )

    # 进行数据转换
    x_encod = preprocessor.fit_transform(x)
    # 获取特征的名字
    feature_names = preprocessor.get_feature_names_out()
    # 由于sklearn的热编码会加上名字(前面定义的one_hot)来区分不同的转换器所以这里把前缀清洗掉
    clean_feature_names = [name.split('__', 1)[1] if '__' in name else name for name in feature_names]
    # 将转换后的特征变为DataFrame并赋予特征名
    x_encod_df = pd.DataFrame(x_encod, columns=clean_feature_names)
    # 记录清洗后的特征信息
    feature_data_df = pd.DataFrame({
        'feature_name': clean_feature_names,
        'datatype': x_encod_df.dtypes.values
    })
    # 向日志输出处理后的特征，并完整地打印数据同时不附带索引
    logger.info(f'源数据处理完成，处理后的特征为:\n{feature_data_df.to_string(index=False)}')
    # 检查热编码后前五行数据
    print(x_encod_df.head().to_string())
    # 返回处理后的数据
    return x_encod_df,y
def feature_selection(x,y,logger,method = 'manual',**kwargs):
    '''
    示例用法：

        示例1：手动保留特征（白名单）
            X_selected, y_selected = feature_selection(
                X, y, logger,
                method='manual',
                keep_feature=['Age', 'MonthlyIncome', 'JobRole_Sales Executive']
            )

        示例2：手动删除特征（黑名单）
            X_selected, y_selected = feature_selection(
                X, y, logger,
                method='manual',
                drop_feature=['EmployeeCount', 'YearsAtCompany']
            )

        示例3：方差过滤（移除方差低于0.01的特征）
            X_selected, y_selected = feature_selection(
                X, y, logger,
                method='variance',
                threshold=0.01
            )

        示例4：相关性筛选（保留与标签相关性绝对值 > 0.05 的特征）
            X_selected, y_selected = feature_selection(
                X, y, logger,
                method='correlation',
                threshold=0.05
            )

        示例5：相关性筛选 + 取Top 20
            X_selected, y_selected = feature_selection(
                X, y, logger,
                method='correlation',
                top_n=20
            )

        示例6：相关性筛选 + 阈值 + Top N（先过滤阈值，再取前N）
            X_selected, y_selected = feature_selection(
                X, y, logger,
                method='correlation',
                threshold=0.03,
                top_n=15
            )

    :param x: 传入进行过OneHot和标准化的数据
    :param y: 传入标签
    :param logger: 传入日志对象
    :param method: 传入你想要使用的方法，有manual(手动选择)和correlation(通过寻找特征与标签的相关性自动寻找)
    :param kwargs: 输入keep_feature = ['特征1','特征2']来获取想要保存的特征
                   或者输入threshold来传入阈值来确定相关性的大小，top_n来确定提取出其中的几个数据
    :return:
    '''
    x_selected = x.copy()
    # 手动选择特征
    if method == 'manual':
        keep_feature = kwargs.get('keep_feature',None)
        drop_feature = kwargs.get('drop_feature',None)
        # 使用特征白名单(适用于要保留的特征少删除的多)
        if keep_feature is not None:
            # 寻找无效特征
            invalid_cols = [col for col in keep_feature if col not in x.columns]
            if invalid_cols:
                logger.error(f'以下特征不存在于源数据:\n{invalid_cols}')
                raise ValueError(f'以下特征不存在于源数据:\n{invalid_cols}')
            x_selected = x[keep_feature]
            logger.info(f'保留了以下特征:\n{keep_feature}')
        # 使用特征黑名单(适用于要删除特征少保留的多)
        elif drop_feature is not None:
            invalid_col = [col for col in drop_feature if col not in x.columns]
            if invalid_col:
                logger.warning(f'以下特征不存在于数据源,已经忽略:\n{invalid_col}')
            valid_drop = [col for col in drop_feature if col in x.columns]
            if valid_drop:
                x_selected = x.drop(columns=valid_drop)
                logger.info(f'删除特征:\n{valid_drop}')
            else:
                logger.warning('无有效特征可删除，数据未变动')
        else:
            logger.info(f'未指定特征工程操作，将保留原特征')
    # 通过方差来选择特征
    elif method == 'variance':
        threshold = kwargs.get('threshold', 0.0)
        selector = VarianceThreshold(threshold=threshold)
        x_transformed = selector.fit_transform(x)
        support = selector.get_support()

        if x_transformed.shape[1] == 0:
            logger.warning(f'⚠️ 方差过滤后无特征保留！threshold={threshold}，返回原数据')
            x_selected = x.copy()
        else:
            selected_cols = x.columns[support].tolist()
            x_selected = pd.DataFrame(x_transformed, columns=selected_cols)
            logger.info(f'✅ 方差过滤后保留 {len(selected_cols)} 个特征: {selected_cols}')

    # 通过与x和y的相关性来选择
    elif method == 'correlation':
        # 获取阈值
        threshold = kwargs.get('threshold',None)
        top_n = kwargs.get('top_n',None)
        # 得到每个特征与标签的相关系数（程度）
        correlations = x.corrwith(y)
        # 取绝对值
        abs_correlations = correlations.abs()
        # 获取超过相关性阈值的特征
        if threshold is not None:
            # 过滤掉弱相关的特征
            mask = abs_correlations >= threshold
            correlations = correlations[mask]
            abs_correlations = abs_correlations[mask]
        # 按相关性绝对值来排行，得到最强的相关性特征（正负相关都有）
        sorted_correlations = correlations.reindex(abs_correlations.sort_values(ascending=False).index)
        # 选取排行后的前n个
        if top_n is not None:
            sorted_correlations = sorted_correlations.head(top_n)
        selected_col = sorted_correlations.index.tolist()
        # 进行赋值并返回
        x_selected = x[selected_col]
        print(f'相关性靠前的为:\n{x_selected.head(10).to_string()}')
        logger.info(f'通过相关性筛选出特征的数据\n:{sorted_correlations.to_string()}')
    return x_selected,y

def save_to_csv(x_df, y_series, label_column='Attrition', output_dir='../output', base_filename='processed_data'):
    """
    将特征DataFrame和标签Series合并并保存为带时间戳的CSV文件，防止重名覆盖
    示例用法：

        示例1：使用默认参数保存
            save_path = save_to_csv(X_selected, y_selected)

        示例2：自定义输出目录和文件名前缀
            save_path = save_to_csv(
                X_selected, y_selected,
                output_dir='../data/processed',
                base_filename='hr_employee_features'
            )

        示例3：自定义标签列名（适用于中文或不同字段名）
            save_path = save_to_csv(
                X_selected, y_selected,
                label_column='是否离职',
                base_filename='离职预测特征集'
            )

    :param x_df: 处理后的特征数据 (pandas.DataFrame)
    :param y_series: 标签数据 (pandas.Series)
    :param label_column: 标签列名，默认为 'Attrition'
    :param output_dir: 输出目录路径，默认为 '../output'
    :param base_filename: 基础文件名（不含扩展名），默认为 'processed_data'
    :return: 实际保存的文件路径
    """
    # 合并特征和标签
    result_df = x_df.copy()
    result_df[label_column] = y_series.values

    # 生成带时间戳的文件名：processed_data_20250912_142730.csv
    timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
    filename = f"{base_filename}_{timestamp}.csv"
    output_path = os.path.join(output_dir, filename)

    # 创建输出目录（如果不存在）
    if output_dir and not os.path.exists(output_dir):
        os.makedirs(output_dir)
        print(f"创建输出目录: {output_dir}")

    # 保存为CSV，不保存索引
    result_df.to_csv(output_path, index=False, encoding='utf-8-sig')
    print(f"数据已保存至: {output_path}")
    print(f"保存数据形状: {result_df.shape} (行数, 列数)")

    return output_path  # 可选：返回路径供后续使

if __name__=='__main__':
    data1 = root_data('../data/train.csv')
    x,y = feature_onehot_standar(data1.data_source,data1.log_file,model_type='linear')
    x_selected,y_selected = feature_selection(x,y,data1.log_file,method='correlation',threshold=0.03)
    save_path = save_to_csv(
        x_selected,
        y_selected,
        label_column='Attrition',
        output_dir='../data',
        base_filename='feature_selection_data'
    )